/*
 * 	<pre>
 *	EasyPeas.
 *  Copyright (C) 2007 S Owen
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Jul 24, 2007
 * InvokerImpl.java
 * </pre>
 */
package org.easypeas.invocation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.easypeas.EasyPeasConstants;
import org.easypeas.exceptions.ForwardNotFoundException;
import org.easypeas.exceptions.ResourceInvocationFailure;
import org.easypeas.exceptions.UnknownResourceException;
import org.easypeas.exceptions.UnresolvedMethodException;
import org.easypeas.mappings.Mappings;

/**
 * This the default implementation of an Invoker.
 * 
 * From the mappings it determines the EasyPea mapped to the first element in
 * the path, and then uses the proceeding parts of the<br>
 * path to invoke accessors (or the special id(String) accessor if no defined
 * accessor matches), except the last element which is used to invoker either an
 * action of a view endpoint.<br>
 * <br>
 * e.g.<br>
 * <code>GET /queue/default/list</code><br>
 * will invoker the accessor mapped to <em>default</em> on the EasyPea mapped
 * to <em>queue</em>. The view method mapped to <em>list</em> is then
 * invoked on the EasyPea returned from that accessor. <br>
 * <code>GET /queue/items/27/name</code> may invoke something like
 * <code>Queue.getItems().id("27").showName(...)</code> <br>
 * If an action the forward is processed to determine the next path.<br>
 * 
 * 
 * 
 * @see Invoker
 * @see View
 * @see Action
 * @see Accessor
 * @see EasyPea
 * 
 * @author S Owen
 */
public class InvokerImpl implements Invoker {
	@SuppressWarnings("unused")
	private static Logger logger = Logger.getLogger(InvokerImpl.class);

	/**
	 * {@inheritDoc}
	 */
	public Map<String, Object> invokePath(HttpServletRequest request,
			HttpServletResponse response, Mappings mapping, String path)
			throws UnresolvedMethodException, UnknownResourceException,
			ResourceInvocationFailure {
		//FIXME: this method needs some major refactoring
		Map<String, Object> map = null;
		String actualPath=path;
		String objectKey = objectKey(actualPath);
		if (actualPath.startsWith("/")) {
			actualPath = actualPath.replaceFirst("/", "");
		}
		actualPath = actualPath.replaceFirst(objectKey, "");
		if (actualPath.startsWith("/"))
			actualPath = actualPath.replaceFirst("/", "");
		Object obj = mapping.getObject(objectKey);
		if (obj == null) {
			throw new UnknownResourceException("Unable to find object for key:"
					+ objectKey);
		}
		if (actualPath.length() > 0) {
			List<String> methods = Arrays.asList(actualPath.split("/"));
			if (logger.isDebugEnabled()) {
				logger.debug("Requested methods found for obj: " + obj
						+ " are: " + methods);
			}
			int i = 1;
			for (String method : methods) {
				logger.debug("Trying method:" + method);
				if (i < methods.size()) {
					boolean success = false;
					try {
						obj = invokeAccessor(obj, mapping.getAccessor(obj,
								method));
						success = true;
					} catch (NoSuchMethodException e) {
						logger.debug("param less method:" + method
								+ "not found on " + obj.getClass());
					}
					if (!success) {
						try {
							Object newObj = invokeById(obj, method);
							if (newObj != null) {
								obj = newObj;
							} else {
								if (logger.isDebugEnabled())
									logger.debug("Object not found for id "
											+ method + " on " + obj.getClass());
								throw new UnresolvedMethodException(
										"Object not found for id " + method
												+ " on " + obj.getClass());
							}
						} catch (NoSuchMethodException e) {
							if (logger.isDebugEnabled()) {
								logger
										.debug("Unable to find suitable method for: "
												+ method
												+ " on "
												+ obj.getClass());
							}
							throw new UnresolvedMethodException(
									"Unable to find suitable method for: "
											+ method + " on " + obj.getClass());
						}
					}
				} else {
					try {
						map = invokeEndpoint(request, response, mapping, obj,
								method, mapping.getView(obj, method));
					} catch (NoSuchMethodException e) {
						try {
							obj = invokeAccessor(obj, mapping.getAccessor(obj,
									method));
							map = invokeEndpoint(request, response, mapping,
									obj, "index", mapping.getView(obj, "index"));
						} catch (NoSuchMethodException e2) {
							try {
								Object newObj = invokeById(obj, method);
								if (newObj != null) {
									obj = newObj;
								} else {
									if (logger.isDebugEnabled()) {
										logger.debug("Object not found for id "
												+ method + " on "
												+ obj.getClass());
									}
									throw new UnresolvedMethodException(
											"Object not found for id " + method
													+ " on " + obj.getClass(),e2);
								}
								map = invokeEndpoint(request, response,
										mapping, obj, "index", mapping.getView(
												obj, "index"));
							} catch (NoSuchMethodException e3) {
								if (logger.isDebugEnabled()) {
									logger
											.debug("Unable to find suitable endpoint for: "
													+ method
													+ " on "
													+ obj.getClass());
								}
								throw new UnresolvedMethodException(
										"Unable to find suitable method for: "
												+ method + " on "
												+ obj.getClass(),e3);
							}
						}
					}
				}
				i++;
			}
		} else {
			try {
				map = invokeEndpoint(request, response, mapping, obj, "index",
						mapping.getView(obj, "index"));
			} catch (NoSuchMethodException e1) {
				if (logger.isDebugEnabled()) {
					logger.debug("No methods, or index, found on "
							+ obj.getClass());
				}
				throw new UnresolvedMethodException(
						"No methods, or index, found on " + obj.getClass(), e1);
			}
		}

		logger.debug("Invocation responded with:" + map);
		return map;
	}

	/**
	 * {@inheritDoc}
	 */
	public Map<String, Object> invokeActionPath(HttpServletRequest request,
			HttpServletResponse response, Mappings mappings, String path)
			throws UnresolvedMethodException, UnknownResourceException,
			ResourceInvocationFailure, ForwardNotFoundException {
		Map<String, Object> result;
		String actualPath = path;
		String objectKey = objectKey(actualPath);
		Object obj = mappings.getObject(objectKey);
		if (actualPath.startsWith("/")) {
			actualPath = actualPath.replaceFirst("/", "");
		}
		actualPath = actualPath.replaceFirst(objectKey, "");
		if (actualPath.startsWith("/")) {
			actualPath = actualPath.replaceFirst("/", "");
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Invoking action " + actualPath + " on " + obj.getClass());
		}

		try {
			Method method = mappings.getAction(obj, actualPath);
			result = invokeEndpoint(request, response, mappings, obj, actualPath,
					method);
		} catch (NoSuchMethodException e) {
			throw new UnresolvedMethodException(e);
		}

		resolveForwardAddress(mappings, actualPath, result, obj);

		return result;
	}

	private void resolveForwardAddress(Mappings mappings, String path,
			Map<String, Object> result, Object obj)
			throws ForwardNotFoundException {
		String forward = (String) result.get(EasyPeasConstants.EASYPEA_FORWARD);
		if (forward == null) {
			throw new ForwardNotFoundException(
					"Unable to find the forward key in the response map");
		}
		forward = mappings.getForward(obj, path, forward);
		if (forward == null) {
			throw new ForwardNotFoundException(
					"Unable to find the forward key in the mappings");
		}
		result.put(EasyPeasConstants.EASYPEA_FORWARD, forward);
	}

	@SuppressWarnings("unchecked")
	private Map<String, Object> invokeEndpoint(HttpServletRequest request,
			HttpServletResponse response, Mappings mappings, Object obj,
			String methodKey, Method method) throws NoSuchMethodException,
			ResourceInvocationFailure {
		if (logger.isDebugEnabled()) {
			logger.debug("Trying method " + method + " with as endpoint "
					+ obj.getClass());
		}
		Map<String, Object> map = new HashMap<String, Object>();
		try {
			String forward = (String) method.invoke(obj, new Object[] {
					request, response, map });
			if (forward != null) {
				map.put(EasyPeasConstants.EASYPEA_FORWARD, forward);
			} else if (mappings.getViewTemplate(method) != null) {
				map.put(EasyPeasConstants.EASYPEA_TEMPLATE, mappings
						.getViewTemplate(method));
			}

		} catch (IllegalArgumentException e) {
			throw new ResourceInvocationFailure(e);
		} catch (IllegalAccessException e) {
			throw new ResourceInvocationFailure(e);
		} catch (InvocationTargetException e) {
			throw new ResourceInvocationFailure(e.getTargetException());
		}
		map.put(EasyPeasConstants.EASYPEA_PEA, obj);
		map.put(EasyPeasConstants.EASYPEA_METHOD, methodKey);
		return map;
	}

	private Object invokeById(Object obj, String id)
			throws ResourceInvocationFailure, NoSuchMethodException {
		if (logger.isDebugEnabled())
			logger
					.debug("Trying to find by id " + id + " on "
							+ obj.getClass());
		Method m;
		try {
			m = obj.getClass().getMethod("id", new Class[] { String.class });
			obj = m.invoke(obj, new Object[] { id });
		} catch (SecurityException e) {
			throw new ResourceInvocationFailure(e);
		} catch (IllegalArgumentException e) {
			throw new ResourceInvocationFailure(e);
		} catch (IllegalAccessException e) {
			throw new ResourceInvocationFailure(e);
		} catch (InvocationTargetException e) {
			throw new ResourceInvocationFailure(e);
		}

		return obj;
	}

	private Object invokeAccessor(Object obj, Method method)
			throws NoSuchMethodException, ResourceInvocationFailure {
		if (logger.isDebugEnabled())
			logger.debug("Trying method " + method + " with no paramers on "
					+ obj.getClass());
		try {
			obj = method.invoke(obj, new Object[] {});
		} catch (IllegalAccessException e) {
			throw new ResourceInvocationFailure(
					"Illegal Access whilst invoking " + method + " on "
							+ obj.getClass(), e);
		} catch (InvocationTargetException e) {
			throw new ResourceInvocationFailure("Failure whilst invoking "
					+ method + " on " + obj.getClass(), e);
		}
		if (logger.isDebugEnabled()) {
			logger.debug("method successfully invoked to recieve new object:"
					+ obj);
		}
		return obj;
	}

	private String objectKey(String path) {
		String result=path;
		if (result.startsWith("/")) {
			result = result.replaceFirst("/", "");
		}
		result = result.split("/")[0];
		if (logger.isDebugEnabled()) {
			logger.debug("Parsed resource key=" + result);
		}
		return result;
	}
}
